--[[
-- sandbox.lua -- Sandbox for safe execution in Lua
--
-- Copyright (C) 2007  Patrick J. Donnelly (batrick@unm.edu)
--
-- This software is distributed under the same license as Lua 5.0:
--
-- Permission is hereby granted, free of charge, to any person obtaining a
-- copy of this software and associated documentation files (the "Software"),
-- to deal in the Software without restriction, including without limitation
-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
-- and/or sell copies of the Software, and to permit persons to whom the
-- Software is furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-- OTHER DEALINGS IN THE SOFTWARE. 
--]]

local dump = string.dump;
local tmpname = os.tmpname;
local task = require("task");

module("sandbox");

local call = "=" .. dump(function()
  -- Getting Parameters
  local task = require("task");
  local parentid = arg[1];
  local serial_file_name = arg[2];
  local stderr = io.stderr;
  local printme = {}; -- Table holds everything we print
  local securef = {}; -- for keeping _G functions from having their
                      -- environments changed
  local PROT = {}; -- Unique variable for protected metatables
  local serialfile = io.open(serial_file_name); -- assuming no errors...

  local lpcall = pcall; -- REMOVE LATERJ L;AKSJDLFJ;ASJDLKFJ498U09218U4
  local lxpcall = xpcall;
  local npcalls = 0;
  local function nmpcalls(...)
    npcalls = npcalls - 1;
    return ...;
  end

  local lrawset = rawset;
  local lrawget = rawget;
  local ltostring = tostring;
  local lpairs = pairs;
  local lnext = next;
  local lsetfenv = setfenv;
  local lopen = io.open; -- for serialization
  local lcoroutine = coroutine;
  local ldebug = debug;
  local lmath = math;
  local los = os;
  local lstring = string;
  local ltable = table;
  local serialize = require("serialize");
  local sdump = serialize.dump;
  require("arena");
  require("decompose");
  require("rot13");

  io = nil;

  debug.getregistry()._LOADED = {};
  debug.getregistry()._LOADLIB = {};
  debug.debug = nil;
  debug.getregistry = nil;
  do
    local setfenv = debug.setfenv;
    debug.setfenv = function(object, table)
      if object ~= nil and securef[object] then
        return error("cannot change secure functions environment", 2);
      else
        return setfenv(object, table);
      end
    end
  end
  do
    local getlocal = debug.getlocal;
    local getinfo = debug.getinfo;
    local NULL = {};
    debug.getlocal = function(thread, level, lcl)
      thread = tonumber(thread) or thread;
      if type(thread) == "thread" then
        level = tonumber(level);
        if level then
          if securef[(getinfo(thread, level) or NULL).func or 0] then
            return error("cannot access secure functions' locals", 2);
          else
            return getlocal(thread, level, lcl);
          end
        else
          return getlocal(thread, level, lcl); -- should error
        end
      elseif type(thread) == "number" and thread > 0 then
        if securef[(getinfo(thread+1) or NULL).func or 0] then
          return error("cannot access secure functions' locals", 2);
        else
          return getlocal(thread+1, level, lcl);
        end
      else
        return getlocal(thread, level, lcl); -- should error in some way
      end
    end
  end
  do
    local setlocal = debug.setlocal;
    local getinfo = debug.getinfo;
    local NULL = {};
    debug.setlocal = function(thread, level, lcl, value)
      thread = tonumber(thread) or thread;
      if type(thread) == "thread" then
        level = tonumber(level);
        if level then
          if securef[(getinfo(thread, level) or NULL).func or 0] then
            return error("cannot modify secure functions' locals", 2);
          else
            return setlocal(thread, level, lcl, value);
          end
        else
          return setlocal(thread, level, lcl, value); -- should error
        end
      elseif type(thread) == "number" and thread > 0 then
        if securef[(getinfo(thread+1) or NULL).func or 0] then
          return error("cannot modify secure functions' locals", 2);
        else
          return setlocal(thread+1, level, lcl, value);
        end
      else
        return setlocal(thread, level, lcl, value); -- should error in some way
      end
    end
  end
  do
    local shareupvalue = debug.shareupvalue;
    debug.shareupvalue = function(func1, n1, func2, n2)
      if func1 and func2 and (securef[func1] or securef[func2]) then
        return error("cannot share secure function's upvalues", 2);
      end
      return shareupvalue(func1, n1, func2, n2);
    end
  end
  do
    local getupvalue = debug.getupvalue;
    debug.getupvalue = function(func, up)
      if type(func) == "function" and securef[func] then
        return error("cannot access secure functions' upvalues", 2);
      end
      return getupvalue(func, up);
    end
  end
  do
    local setupvalue = debug.setupvalue;
    debug.setupvalue = function(func, up, value)
      if type(func) == "function" and securef[func] then
        return error("cannot change secure functions' upvalues", 2);
      end
      return setupvalue(func, up, value);
    end
  end
  do
    local setmetatable = debug.setmetatable;
    local getmetatable = debug.getmetatable;
    debug.setmetatable = function(object, tab)
      local m = getmetatable(object);
      if m and lrawget(m, PROT) then
        return error("cannot modify protected metatable", 2);
      end
      return setmetatable(object, tab);
    end
  end
  do
    local getmetatable = debug.getmetatable;
    debug.getmetatable = function(object)
      local m = getmetatable(object);
      if m and lrawget(m, PROT) then
        return error("cannot access protected metatable", 2);
      end
      return m;
    end
  end

  os.exit = nil;
  os.execute = nil;
  os.getenv = nil;
  os.remove = nil;
  os.rename = nil;
  os.setlocale = nil;
  os.tmpname = nil; -- makes an empty file :(

  table.foreach = function(t, f)
    assert(type(t) == "table",
        ("bad argument #1 to 'foreach' (table expected, got %s)"):format(t));
    assert(type(f) == "function",
        ("bad argument #1 to 'foreach' (function expected, got %s)"):format(t));
    for k,v in pairs(t) do -- use environment pairs
      local brk = f(k,v);
      if brk then
        return brk;
      end
    end
  end

  local stringm = getmetatable("");
  stringm.__metatable = "leave this alone";
  stringm[PROT] = true;

  -- for 'tabstr' function
  local function totype(x)
    local t = type(x);
    if t == "string" then
      return color('"'.. x .. '"', "red");
    elseif t == "number" then
      return x;
    else
      return tostring(x);
    end
  end
  local colors = {
    white = "0",
    black = "1",
    blue = "2",
    green = "3",
    red = "4",
    brown = "5",
    purple = "6",
    orange = "7",
    yellow = "8",
    ["light green"] = "9",
    teal = "10",
    ["light cyan"] = "11",
    ["light blue"] = "12",
    pink = "13",
    grey = "14",
    ["light grey"] = "15"
  };

 -- creating the secure environment
  local tconcat = {}; -- for print()
  local tnext = {}; -- transfer table
  setmetatable(tnext, {__index = function(t, k) return k end});

  local secureenv;
  secureenv = {
    _G = _G,
    _VERSION = _VERSION,
    assert = assert,
    collectgarbage = collectgarbage,
    error = error,
    gcinfo = gcinfo,
    getfenv = getfenv,
    getmetatable = getmetatable,
    ipairs = ipairs,
    load = load,
    loadstring = loadstring,
    --newproxy = newproxy,
    next = function(t, k, ...)
      local next = lnext;
      if t == _G then
        if secureenv[k] then
          return next(secureenv, k, ...);
        else
          local k, v = next(t, k, ...);
          if k then
            return k, v;
          else
            return next(secureenv, nil);
          end
        end
      else
        return next(tnext[t], k, ...);
      end
    end,
    _OWNER = "My owner is batrick. "..
            "He can be found on the Freenode network.",
    pairs = function(tab)
      local t = type(tab);
      if t ~= "table" then
        return error((
              "bad argument #1 to 'pairs' (table expected, got %s)"):format(
              t), 2);
      end
      return next, tab, nil;
    end,
    pcall = function(...)
      if npcalls > 10 then
        return false, "ead shirik";
      else
        npcalls = npcalls + 1;
        return nmpcalls(lpcall(...));
      end
    end,
    print = function(...)
      tconcat.n = select("#", ...);
      for i = 1, tconcat.n do
        tconcat[i] = tostring((select(i, ...)));
      end
      printme[#printme + 1] = table.concat(tconcat, "  ");
      for k in ipairs(tconcat) do
        tconcat[k] = nil;
      end
    end,
    rawequal = rawequal,
    rawget = function(t, k, ...)
      local rawget = lrawget;
      if t == _G or tnext[t] ~= t then
        return t[k];
      else
        return rawget(t, k, ...);
      end
    end,
    rawset = function(t, k, v, ...)
      local rawset = lrawset;
      if t == _G and secureenv[k] or tnext[t] ~= t then
        return error("cannot perform rawset on protected variables", 2);
      else
        return rawset(t, k, v, ...);
      end
    end,
    select = select,
    setfenv = function(f, t, ...)
      local setfenv = lsetfenv;
      f = tonumber(f) or f;
      if type(f) == "number" and f > 0 then
        return setfenv(f + 1, t, ...);
      elseif f ~= nil and securef[f] then
        return error("cannot change secure functions environment", 2);
      else
        return setfenv(f, t, ...);
      end
    end,
    setmetatable = setmetatable,
    tonumber = tonumber,
    tostring = tostring,
    type = type,
    unpack = unpack,
    xpcall = function(...)
      if npcalls > 10 then
        return false, "ead shirik";
      else
        npcalls = npcalls + 1;
        return nmpcalls(lxpcall(...));
      end
    end,

    -- my functions
    points = arena.points,
    rating = arena.rating,
    decompose = decompose,
    rot13 = rot13,
    newenv = function()
      local _G = _G;
      for k in lpairs(_G) do
        _G[k] = nil;
      end
    end,
    printf = function(sformat, ...)
      local t = type(sformat);
      if t ~= "string" then
        return error(("bad argument #1 to 'printf' (string expected, got %s)"
              ):format(t), 2);
      else
        local f = sformat:format(...);
        print(f);
        return f;
      end
    end,
    tabstr = function(tab, lookup)
      local t = type(tab);
      if t ~= "table" then
        return error((
              "bad argument #1 to 'tabstr' (table expected, got %s)"):format(
              t), 2);
      else
        lookup = lookup or {};
        lookup[tab] = true;
        local buffer = {};
        for k,v in pairs(tab) do
          buffer[#buffer+1] = "[" .. totype(k) .. "] = " ..
              (type(v) == "table" and not lookup[v] and
               tabstr(v, lookup) or tostring(v));
        end
        return color("{", "blue") .. " " .. table.concat(buffer, ", ") ..
            " " .. color("}", "blue");
      end
    end,
    color = function(str, color)
      local c = colors[color];
      if not c then
        return error("invalid color", 2);
      else
        return "\3" .. c .. str .. "\3";
      end
    end,

    coroutine = setmetatable({}, {
      __metatable = "leave this alone",
      __index = lcoroutine,
      __newindex = function()
        error("cannot modify 'coroutine' table", 2);
      end,
     [PROT] = true,
    }),
    debug = setmetatable({}, {
      __metatable = "leave this alone",
      __index = ldebug,
      __newindex = function()
        error("cannot modify 'debug' table", 2);
      end,
     [PROT] = true,
    }),
    math = setmetatable({}, {
      __metatable = "leave this alone",
      __index = lmath,
      __newindex = function()
        error("cannot modify 'math' table", 2);
      end,
     [PROT] = true,
    }),
    os = setmetatable({}, {
      __metatable = "leave this alone",
      __index = los,
      __newindex = function()
        error("cannot modify 'os' table", 2);
      end,
     [PROT] = true,
    }),
    string = setmetatable({}, {
      __metatable = "leave this alone",
      __index = lstring,
      __newindex = function()
        error("cannot modify 'string' table", 2);
      end,
     [PROT] = true,
    }),
    table = setmetatable({}, {
      __metatable = "leave this alone",
      __index = ltable,
      __newindex = function()
        error("cannot modify 'table' table", 2);
      end,
     [PROT] = true,
    }),
  };

  setmetatable(_G, {
    __metatable = "leave this alone",
    __index = secureenv,
    __newindex = function(t, k, v)
      if secureenv[k] then
        return error("cannot modify secure global variables", 2);
      else
        lrawset(t, k, v);
      end
    end,
   [PROT] = true,
  });
  
  newenv();

  -- Protecting the functions from having their environments
  -- changed by placing them in securef table so setfenv
  -- can check it.
  for k,v in lpairs(secureenv) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end

  for k,v in lpairs(lcoroutine) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end
  tnext[coroutine] = lcoroutine;
  for k,v in lpairs(ldebug) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end
  tnext[debug] = ldebug;
  for k,v in lpairs(lmath) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end
  tnext[math] = lmath;
  for k,v in lpairs(los) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end
  tnext[os] = los;
  for k,v in lpairs(lstring) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end
  tnext[string] = lstring;
  for k,v in lpairs(ltable) do
    if type(v) == "function" then
      securef[v] = true;
    end
  end
  tnext[table] = ltable;

  serialize.loadlibs();

  local function twa(...)
    return {n = select("#", ...), ...};
  end
  securef[twa] = true;

  local function dump(str)
    return loadstring((string.gsub(str, "^%s*=", "return ")));
  end
  securef[dump] = true;

  local function serialize_G()
    serialfile:close();
    serialfile = lopen(serial_file_name, "w+");

    local b, result = pcall(sdump, _G, "_G", serialfile, 1000);
    if not b then
      -- this is cleared out anyway
      print("Problem serializing data: "..result); -- Next one is run-time error
    end
  end
  securef[serialize_G] = true;

  -- Now we load the serialized global state from earlier
  local sfile = serialfile:read("*a");
  local sf, err = loadstring(sfile);
  if not sf then
    print("problem loading serial file: "..err);
    stderr:write("Problem loading file:\n", sfile);
  else
    local b, err = pcall(sf);
    if not b then
      print("problem executing serial file: "..err);
      stderr:write("Problem executing serial file:\n", sfile);
    end
  end

  local tostring = tostring;
  local concat = table.concat;
  local sub = string.sub;
  local function get_results(printme, returns)
    if not returns[1] then
      -- runtime error
      return false, tostring(returns[2]);
    else
      local p, r;
      if #printme > 0 then
        for i = 1, #printme do
          printme[i] = sub(tostring(printme[i]), 1, 250);
        end
        p = concat(printme, "\t");
      end
      if returns.n > 1 then
        for i = 2, returns.n do
          returns[i] = sub(tostring(returns[i]), 1, 250);
        end
        r = concat(returns, ", ", 2);
      end
      return true, p, r;
    end
  end
  securef[get_results] = true;

  local receive = task.receive;
  local post = task.post;
  local loadstring = loadstring;
  local collectgarbage = collectgarbage;
  local resume = coroutine.resume;
  local create = coroutine.create;
  local newenv = newenv;
  local function begin()
    while true do
      local msg, flags, rc = receive(-1); -- Waits for something
      if flags == -1 then
        break;
      else
        -- msg is what we loadstring
        -- is it just an expression?
        local func, err = loadstring("return "..msg);
        if not func then
          -- try what's regular...
          func, err = dump(msg);
        end

        if not func then
          post(parentid, tostring(returns[2]), -2);
        else
          -- No compile errors...
          local co = create(func);
          local returns = twa(resume(co));

          -- They may have made bad tostring metamethods, compute
          -- results in an auxiliary thread.
          local gr = create(get_results);
          local status, err, p, r = resume(gr, printme, returns);

          if not status then
            post(parentid, "error when getting results", -2);
          elseif not err then
            post(parentid, p, -2);
          else
            if r then
              post(parentid, r, 1);
            end
            if p then
              post(parentid, p, 2);
            end  
          end

          -- serialize the environment
          serialize_G();
          collectgarbage("restart");
          collectgarbage("setpause", 100);
          collectgarbage("setstepmul", 200);
        end
      end
      -- cleanup
      for i = 1, #printme do
        printme[i] = nil;
      end
      if collectgarbage("count") > 75000 then
        collectgarbage("collect");
        collectgarbage("collect");-- Need to perform two consecutive sweeps
        if collectgarbage("count") > 50000 then -- they are eating too much
          newenv();
          collectgarbage("collect");
          collectgarbage("collect");-- Need to perform two consecutive sweeps
        end
      end
    end
  end
  securef[begin] = true;
  return begin();
end);

local main = task.id();
local serial_file_name = tmpname();
local tsk = task.create(call, {main, serial_file_name});

function safecall(str)
  if not task.isrunning(tsk) then
    tsk = task.create(call, {main, serial_file_name});
    if tsk < 0 then return nil; end
  end
  task.post(tsk, str, 0);

  local msg, flag, rc = task.receive(5000);

  if rc == 0 then
    if flag < 0 then
      -- error
      return flag, msg;
    else
      local answers = { };

      -- regular?
      repeat
        answers[flag] = msg;
        msg, flag, rc = task.receive(50);
      until rc == -2 or flag == 0;
      return 0, answers;
    end
  elseif rc == -2 then
    task.cancel(tsk); -- It's been running too long
    return -2, "maximum execution time/memory exceeded";
  else
    return nil;
  end
end
